﻿// --------------------------------
// <copyright file="MenuManager.cs" company="Faculty of Information Technology, Czech Technical University in Prague">
//     This document is shared by Microsoft Public License (MS-PL)
//     For further details please see http://msdn.microsoft.com/en-us/library/ff648068.aspx or license.txt.
// </copyright>
// <author>Lukáš Molnár</author>

// <email>molnaluk@fit.cvut.cz</email>

// <date>1. 12. 2011 15:13:59</date>

// <summary>
//
// </summary>
// --------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input;
using System.IO;
using FITClub.Episode1.Configuration;
using FITClub.Episode1;
using FITClub.Util;
using Microsoft.Xna.Framework.Input.Touch;

#if WINDOWS

using Nuclex.Input;
using ExternalClasses.Controls;
using FITClub.Episode1.Levels;
using System.ComponentModel;

#endif

namespace FITClub.TextureMenu
{
    /// <summary>
    /// Manages loading, updating, drawing and storing the background, active and inactive items.
    /// </summary>
    [XmlInclude(typeof(BackgroundLayer))]
    [XmlInclude(typeof(BackgroundLayerAnimated))]
    public class MenuManager
    {
        #region Static properties

        public static double ScaleX { get; private set; }

        public static double ScaleY { get; private set; }

        public static MenuManager Current { get; private set; }

        #endregion Static properties

        #region Public Properties

        #region Serialized properties

        [XmlElement]
        public int BaseWidth { get; set; }

        [XmlElement]
        public int BaseHeight { get; set; }

        [XmlElement]
        public int MoveCoefficient { get; set; }

        [XmlArray]
        [XmlArrayItem("Path")]
        public string[] MenuPaths { get; set; }

        [XmlArray]
        [XmlArrayItem("Path")]
        public string[] PopupPaths { get; set; }

        [XmlArray]
        [XmlArrayItem(typeof(BackgroundLayer))]
        [XmlArrayItem(Type = typeof(BackgroundLayerAnimated), ElementName = "AnimatedLayer")]
        public BackgroundLayer[] BackgroundLayers { get; set; }

        [XmlElement]
        public string ActivePopUp { get; set; }

        #endregion Serialized properties

        [XmlIgnore]
        public BackgroundLayer[] AnimatedBackgroundLayers { get; set; }

        public string MessageBoxPath { get; set; }

        [XmlIgnore]
#if WINDOWS
        [Browsable(false)]
#endif
        public bool ContentLoaded { get; set; }

        [XmlIgnore]
        public Dictionary<string, ADisplayable> Displayables { get; set; }

        [XmlIgnore]
        public SpriteFont NormalFont { get; private set; }

        [XmlIgnore]
        public Texture2D FadeTexture { get; private set; }

        public event Action<string> PopUpHidden;
        public event Action<string, Exception> NavigationFailed;

        #region Input states

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public KeyboardState ActualKeyboardState { get; private set; }

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public KeyboardState LastKeyboardState { get; set; }

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public GamePadState ActualGamepadState { get; private set; }

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public GamePadState LastGamepadState { get; set; }

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public List<GestureSample> ActualGestureSamples { get; private set; }

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public List<GestureSample> LastGestureSamples { get; set; }

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public TouchCollection ActualTouches { get; private set; }

#if WINDOWS

        [Browsable(false)]
#endif
        [XmlIgnore]
        public TouchCollection LastTouches { get; set; }

#if WINDOWS

        [XmlIgnore]
        [Browsable(false)]
        public PCGamePadState ActualPCGamepadState { get; private set; }

        [XmlIgnore]
        [Browsable(false)]
        public PCGamePadState LastPCGamepadState { get; set; }

#endif

        [XmlIgnore]
#if WINDOWS
        [Browsable(false)]
#endif
        public MouseState ActualMouseState { get; private set; }

        [XmlIgnore]
#if WINDOWS
        [Browsable(false)]
#endif
        public MouseState LastMouseState { get; set; }

        #endregion Input states

        [XmlIgnore]
        public Dictionary<string, Menu> Menus { get; private set; }

        [XmlIgnore]
        public Dictionary<string, Menu> PopUps { get; private set; }

        [XmlIgnore]
        public string ActualMenu { get; set; }

        [XmlIgnore]
#if WINDOWS
        [Browsable(false)]
#endif
        public SpriteBatch SpriteBatch { get; set; }

        [XmlIgnore]
#if WINDOWS
        [Browsable(false)]
#endif
        public Game Game { get; private set; }

        [XmlIgnore]
#if WINDOWS
        [Browsable(false)]
#endif
        public GraphicsDevice GraphicsDevice { get; set; }

        [XmlIgnore]
        public bool IsMoving { get { return this.next != null; } }

        [XmlIgnore]
        public bool DisplayPopUp { get; private set; }

        [XmlIgnore]
        public Menu this[string menuName]
        {
            get { return this.Menus[menuName]; }
        }

        #endregion Public Properties

        #region Private fields

        [XmlIgnore]
        private ContentManager loader;
        [XmlIgnore]
        internal int cycles;

        private Point deltaMenu;
        private int moveCycles;
        private int deltaDir = -1;
        private Point oldDisplaySize;
        private Menu next;
        private Stack<Menu> history;
        private Label messageBoxLabel;
        private ImageItem messageBoxInfo;
        private ImageItem messageBoxWarning;
        private Button messageBoxButton1;
        private bool modalPopup;
#if WINDOWS
        private InputManager input;
#endif

        #endregion Private fields

        public MenuManager()
        {
            this.Menus = new Dictionary<string, Menu>();
            this.PopUps = new Dictionary<string, Menu>();
            this.Displayables = new Dictionary<string, ADisplayable>(64);

            if (MenuManager.Current != null)
            {
                MenuManager.Current.Dispose();
            }
            MenuManager.Current = this;
            this.history = new Stack<Menu>(8);
            if (MenuManager.ScaleX == 0) MenuManager.ScaleX = 1;
            if (MenuManager.ScaleY == 0) MenuManager.ScaleY = 1;
        }

        public void LoadContent(ContentManager loader)
        {
            this.loader = loader;
            if (ContentLoaded == false)
            {
#if WINDOWS
                this.input = PCGamePad.input;
#endif
                FadeTexture = new Texture2D(MenuManager.Current.GraphicsDevice, 1, 1);
                FadeTexture.SetData(new Color[] { Color.FromNonPremultiplied(8, 8, 8, 192) });

                this.NormalFont = loader.Load<SpriteFont>(@"menu\fonts\normal1");
                this.BackgroundLayers = (from bl in this.BackgroundLayers orderby bl.Order select bl).ToArray();
                this.AnimatedBackgroundLayers = (from bl in this.BackgroundLayers where bl is BackgroundLayerAnimated orderby bl.Order select bl).ToArray();
                if (this.PopupPaths == null)
                {
                    this.PopupPaths = new string[0];
                }
                foreach (var item in this.MenuPaths)
                {
                    this.LoadScreen(loader, item, this.Menus);
                }

                if (this.MessageBoxPath == null || this.MessageBoxPath.Trim() == string.Empty)
                {
                    this.MessageBoxPath = "Menu\\MessageBox.xml";
                }

                this.LoadScreen(loader, this.MessageBoxPath, this.PopUps);
                foreach (var item in PopupPaths)
                {
                    this.LoadScreen(loader, item, this.PopUps);
                }

                this.messageBoxInfo = this.Displayables["infoIconMBox"] as ImageItem;
                this.messageBoxWarning = this.Displayables["warningIconMBox"] as ImageItem;
                this.messageBoxLabel = this.Displayables["labelMBox"] as Label;
                this.messageBoxButton1 = this.Displayables["buttonMBox1"] as Button;

                foreach (var item in this.BackgroundLayers)
                {
                    item.LoadContent(loader);
                }

                // backgroundRect = new Rectangle(0, 0, BackgroundTexture.Width, Graphics.PreferredBackBufferHeight);

                AMenuItemList<string> list = Displayables["listChangeResolution"] as AMenuItemList<string>;
                list.Options.Clear();
                var dmodes = from dm in SpriteBatch.GraphicsDevice.Adapter.SupportedDisplayModes
                             where dm.Width > dm.Height
                             orderby dm.Width, dm.Height
                             select new Point { X = dm.Width, Y = dm.Height };

                foreach (var item in dmodes.Distinct())
                {
                    list.Options.Add(string.Format("{0}x{1}", item.X, item.Y));
                }
                this.ContentLoaded = true;
                this.NextMenu(this.Menus.Values.First().Name);
                TouchPanel.EnabledGestures = GestureType.Tap | GestureType.HorizontalDrag | GestureType.VerticalDrag;
            }
        }

        private void LoadScreen(ContentManager loader, string item, Dictionary<string, Menu> dict)
        {
            Menu m = Menu.DeserializeXML(item);
            m.LoadContent(loader);
            dict.Add(m.Name, m);
            foreach (var itm in m.MenuItems)
            {
                this.Displayables.Add(itm.Name, itm);
            }
            foreach (var itm in m.InactiveItems)
            {
                this.Displayables.Add(itm.Name, itm);
            }
        }

        /// <summary>
        /// Releases all loaded resources.
        /// </summary>
        public void Dispose()
        {
            foreach (var item in this.Menus.Values)
            {
                item.Dispose();
            }
            foreach (var item in this.PopUps.Values)
            {
                item.Dispose();
            }
            this.PopUps.Clear();
            this.Displayables.Clear();
            this.Menus.Clear();
            foreach (var item in this.BackgroundLayers)
            {
                item.Dispose();
            }
            this.ContentLoaded = false;
        }

        public void ResetInput()
        {
            this.BeginUpdate();
            this.FinalizeUpdate();
        }

        #region Input testers

        /// <summary>
        /// Checks if is a key pressed.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="c">How many cycles have to be ignored since last IsKeyDown.</param>
        /// <returns></returns>
        public bool HitTest(Keys key, int c)
        {
            if (ActualKeyboardState.IsKeyDown(key))
            {
                if (LastKeyboardState.IsKeyDown(key) == false || cycles > c)
                {
                    cycles = 0;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Checks if is a XBOX gamepad button pressed.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="c">How many cycles have to be ignored since last IsButtonDown.</param>
        /// <returns></returns>
        public bool HitTest(Buttons key, int c)
        {
            if (ActualGamepadState.IsButtonDown(key))
            {
                if (LastGamepadState.IsButtonDown(key) == false || cycles > c)
                {
                    cycles = 0;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Is any TouchLocation within given rectangle?
        /// </summary>
        /// <param name="bounds"></param>
        /// <returns></returns>
        public bool TapTest(Rectangle bounds)
        {
            foreach (var item in this.ActualGestureSamples)
            {
                if (item.GestureType == GestureType.Tap && bounds.ContainsRounded(item.Position))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="bounds"></param>
        /// <returns></returns>
        public bool MouseUpTest(Rectangle bounds)
        {
            return this.LastMouseState.LeftButton == ButtonState.Pressed
                && this.ActualMouseState.LeftButton == ButtonState.Released
                && bounds.ContainsRounded(new Vector2(this.LastMouseState.X, this.LastMouseState.Y))
                && bounds.ContainsRounded(new Vector2(this.ActualMouseState.X, this.ActualMouseState.Y));
        }

        public bool MouseDownTest(Rectangle bounds)
        {
            return this.ActualMouseState.LeftButton == ButtonState.Pressed
                && bounds.ContainsRounded(new Vector2(this.ActualMouseState.X, this.ActualMouseState.Y));
        }

#if WINDOWS

        /// <summary>
        /// Checks if is XBOX gamepad button pressed.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="c">How many cycles have to be ignored since last IsButtonDown.</param>
        /// <returns></returns>
        public bool HitTest(PCGamePadButtons key, int c)
        {
            if (ActualPCGamepadState.IsButtonDown(key))
            {
                if (LastPCGamepadState.IsButtonDown(key) == false || cycles > c)
                {
                    cycles = 0;
                    return true;
                }
            }
            return false;
        }

#endif

        private void ReadTouchInput()
        {
            this.ActualTouches = TouchPanel.GetState();
            this.ActualGestureSamples = new List<GestureSample>(8);
            while (TouchPanel.IsGestureAvailable)
            {
                this.ActualGestureSamples.Add(TouchPanel.ReadGesture());
            }
        }

        #endregion Input testers

        /// <summary>
        /// Update input states and menu or slightly move to next screen.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime)
        {
            this.BeginUpdate();
            if (this.DisplayPopUp && this.PopUps.ContainsKey(this.ActivePopUp))
            {
                this.PopUps[this.ActivePopUp].Update(gameTime);
                this.cycles++;
                this.FinalizeUpdate();
                return;
            }

            if (this.next == null)
            {
                this[this.ActualMenu].Update(gameTime);
                foreach (var item in this.AnimatedBackgroundLayers)
                {
                    item.Update(gameTime);
                }
                this.cycles++;
            }
            else
            {
                //// posuvacia logika
                if (this.moveCycles < this.MoveCoefficient)
                {
                    this[this.ActualMenu].Move(this.deltaMenu);
                    this.next.Move(this.deltaMenu);

                    foreach (var item in this.BackgroundLayers)
                    {
                        item.Update(gameTime);
                    }
                    this.moveCycles++;
                }
                else
                {
                    this.moveCycles = 0;
                    int xxxx = this.GraphicsDevice.Viewport.Width * this.MoveCoefficient;
                    foreach (var item in this.BackgroundLayers)
                    {
                        item.Bounds.Offset(-next.Position.X, 0);
                        if (item.Direction == 1)
                        {
                            item.Bounds = new Rectangle(0, item.Bounds.Y, item.Bounds.Width, item.Bounds.Height);
                        }
                        else
                        {
                            item.Bounds = new Rectangle(-xxxx / item.MoveCoefficient, item.Bounds.Y, item.Bounds.Width, item.Bounds.Height);
                        }
                        item.Direction *= -1;
                    }
                    this[this.ActualMenu].SetPosition(Point.Zero);
                    this.next.SetPosition(Point.Zero);

                    this.ActualMenu = this.next.Name;
                    this.next = null;
                    deltaDir *= -1;
                }
            }
            this.FinalizeUpdate();
        }

        private void BeginUpdate()
        {
            this.ActualKeyboardState = Keyboard.GetState();
            this.ActualGamepadState = GamePad.GetState(PlayerIndex.One);
            this.ActualMouseState = Mouse.GetState();
            this.ReadTouchInput();
#if WINDOWS
            this.ActualPCGamepadState = PCGamePad.AnyPCGamePadGetState();
#endif
        }

        private void FinalizeUpdate()
        {
            this.LastMouseState = this.ActualMouseState;
            this.LastGamepadState = this.ActualGamepadState;
            this.LastKeyboardState = this.ActualKeyboardState;
            this.LastGestureSamples = this.ActualGestureSamples;
            this.LastTouches = this.ActualTouches;
#if WINDOWS
            this.LastPCGamepadState = this.ActualPCGamepadState;
#endif
        }

        public void Draw(GameTime gt)
        {
            this.SpriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
            this.Draw(this.SpriteBatch, Color.White);
            this.SpriteBatch.End();
        }

        public void Draw(SpriteBatch spriteBatch, Color tint)
        {
            foreach (var item in BackgroundLayers)
            {
                item.Draw(spriteBatch, tint);
            }
            this[this.ActualMenu].Draw(spriteBatch, tint);
            if (this.IsMoving)
            {
                next.Draw(spriteBatch, tint);
            }
            if (this.DisplayPopUp && this.ActivePopUp != null && this.PopUps.ContainsKey(this.ActivePopUp))
            {
                spriteBatch.Draw(this.FadeTexture, spriteBatch.GraphicsDevice.Viewport.Bounds, tint);
                this.PopUps[this.ActivePopUp].Draw(spriteBatch, tint);
            }
        }

        /// <summary>
        /// Screen resolution has changed.
        /// </summary>
        public void Resized()
        {
#if WINDOWS_PHONE == flase
            double deltaRatioX = ((double)this.GraphicsDevice.Viewport.Width) / ((double)this.oldDisplaySize.X);
            double deltaRatioY = ((double)this.GraphicsDevice.Viewport.Height) / ((double)this.oldDisplaySize.Y);
            MenuManager.ScaleX = (float)this.GraphicsDevice.Viewport.Width / this.BaseWidth;
            MenuManager.ScaleY = (float)this.GraphicsDevice.Viewport.Height / this.BaseHeight;

            if (deltaRatioX != 1)
            {
                foreach (var item in this.BackgroundLayers)
                {
                    item.Resized(deltaRatioX, deltaRatioY);
                }
            }
            if (deltaRatioX != 1 || deltaRatioY != 1)
            {
                foreach (var item in this.Menus.Values)
                {
                    item.Resized(deltaRatioX, deltaRatioY);
                }
            }
#endif
            this.SetOldSize();
        }

        /// <summary>
        /// Return to previous menu screen.
        /// </summary>
        public void Back()
        {
            if (this.DisplayPopUp == false)
            {
                try
                {
                    NextMenu(history.Peek().Name);
                }
                catch (Exception exc)
                {
                    if (this.NavigationFailed != null)
                    {
                        this.NavigationFailed(null, exc);
                    }
                }
            }
            else
            {
                if (this.modalPopup == false)
                {
                    this.DisplayPopUp = false;
                }
            }
        }

        /// <summary>
        /// Switch menu screen with paralax effect and remember history.
        /// </summary>
        /// <param name="menuName">Menu identifier which is going to display.</param>
        public void NextMenu(string menuName)
        {
            this.next = Menus[menuName];
            if (ActualMenu != null)
            {
                try
                {
                    this[this.ActualMenu].Deactivated();
                    if (history.Peek() == next)
                    {
                        history.Pop();
                    }
                    else
                    {
                        history.Push(this[ActualMenu]);
                    }
                }
                catch
                {
                    history.Push(this[ActualMenu]);
                }
                Point v2 = new Point(-1 * deltaDir * GraphicsDevice.Viewport.Width, 0);
                next.SetPosition(v2);
                deltaMenu = new Point(deltaDir * GraphicsDevice.Viewport.Width / this.MoveCoefficient, 0);
                moveCycles = 0;
            }
            else
            {
                ActualMenu = next.Name;
                next = null;
            }
            this[menuName].Activated();
        }

        /// <summary>
        /// Clear the menu passage.
        /// </summary>
        public void ClearHistory()
        {
            this.history.Clear();
        }

        /// <summary>
        /// Exit game called from menu action.
        /// </summary>
        public void Exit()
        {
            this.Game.Exit();
        }

        public void ReloadManager()
        {
            this.Displayables.Clear();
            this.Menus.Clear();
            this.ContentLoaded = false;
            this.LoadContent(loader);
            this.oldDisplaySize = new Point(this.BaseWidth, this.BaseHeight);
            this.Resized();
        }

        public void SetOldSize()
        {
            this.oldDisplaySize = new Point(this.GraphicsDevice.Viewport.Width, this.GraphicsDevice.Viewport.Height);
        }

        public static MenuManager Load(Stream input, Game game, GraphicsDevice graphicsDevice, SpriteBatch spriteBatch, ContentManager loader)
        {
            MenuManager mm = null;

            XmlSerializer serializer = new XmlSerializer(typeof(MenuManager));
            mm = serializer.Deserialize(input) as MenuManager;
            mm.Game = game;
            mm.GraphicsDevice = graphicsDevice;
            mm.SpriteBatch = spriteBatch;
            mm.SetOldSize();
//#if WINDOWS_PHONE
            mm.MoveCoefficient /= 2;
//#endif
            mm.LoadContent(loader);
            return mm;
        }

        public static MenuManager Load(string filename, Game game, GraphicsDevice graphicsDevice, SpriteBatch spriteBatch, ContentManager loader)
        {
            using (Stream input = TitleContainer.OpenStream(filename))
            {
                return Load(input, game, graphicsDevice, spriteBatch, loader);
            }
        }

        /// <summary>
        /// Switches menu screen without effect and optionally clears history.
        /// </summary>
        /// <param name="name"></param>
        public void SetMenu(string name, bool clearHist)
        {
            this[this.ActualMenu].Deactivated();
            this.ActualMenu = name;
            this[this.ActualMenu].Activated();
            this[this.ActualMenu].SetPosition(Point.Zero);
            if (clearHist) this.history.Clear();
        }

        public void SetMenu(string name)
        {
            SetMenu(name, true);
        }

        /// <summary>
        /// Serializes manager and every menu and pop-up screen into specified folder.
        /// </summary>
        /// <param name="folder"></param>
        public void SaveEverithing(string folder)
        {
#if WINDOWS
            string relativeFolder = ContentPathEditor.StripContentFolderPath(folder);
            this.MenuPaths = (from itm in this.Menus.Values select Path.Combine(relativeFolder, itm.Name + ".xml")).ToArray();
            this.PopupPaths = (from itm in this.PopUps.Values select Path.Combine(relativeFolder, itm.Name + ".xml")).ToArray();
#else
            this.MenuPaths = (from itm in this.Menus.Values select Path.Combine("Menu", itm.Name + ".xml")).ToArray();
            this.PopupPaths = (from itm in this.PopUps.Values select Path.Combine("Menu", itm.Name + ".xml")).ToArray();
#endif

            foreach (var item in this.Menus.Values)
            {
                item.SerializeXML(folder);
            }
            using (var fs = new FileStream(Path.Combine(folder, "MenuManager.xml"), FileMode.Create, FileAccess.Write))
            {
                XmlSerializer xser = new XmlSerializer(this.GetType());
                xser.Serialize(fs, this);
            }
        }

        public void HidePopup()
        {
            bool fire = this.DisplayPopUp;
            this.DisplayPopUp = false;
            if (fire && this.PopUpHidden != null)
            {
                this.PopUpHidden(this.ActivePopUp);
            }
        }

        public void ShowPopup(string popupName)
        {
            this.ActivePopUp = popupName;
            this.DisplayPopUp = true;
        }

        public void ShowMessageBox(string text, bool warning)
        {
            this.messageBoxButton1.Visible = true;
            this.messageBoxLabel.Text = text;
            this.messageBoxInfo.Visible = !warning;
            this.messageBoxWarning.Visible = warning;
            this.modalPopup = false;
            this.ShowPopup("MessageBox");
        }

        public void ShowModalMessageBox(string text, bool warning)
        {
            this.ShowMessageBox(text, warning);
            this.messageBoxButton1.Visible = false;
            this.modalPopup = true;
        }
    }
}